package main;

import bean.RawMessage;
import bean.RawMessage.MessageStatus;
import bean.RawMessage.MessageType;
import bean.SMSCNodeConfiguration;
import com.logica.smpp.Data;
import com.logica.smpp.ServerPDUEvent;
import com.logica.smpp.ServerPDUEventListener;
import com.logica.smpp.Session;
import com.logica.smpp.TCPIPConnection;
import com.logica.smpp.pdu.BindRequest;
import com.logica.smpp.pdu.BindResponse;
import com.logica.smpp.pdu.BindTransciever;
import com.logica.smpp.pdu.DeliverSM;
import com.logica.smpp.pdu.DeliverSMResp;
import com.logica.smpp.pdu.EnquireLinkResp;
import com.logica.smpp.pdu.PDU;
import com.logica.smpp.pdu.Response;
import com.logica.smpp.pdu.ShortMessage;
import com.logica.smpp.pdu.SubmitSM;
import com.logica.smpp.pdu.SubmitSMResp;
import com.logica.smpp.util.ByteBuffer;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.apache.log4j.Logger;

/**
 *
 * @author enemY
 */
public class SMSCConnection extends Thread implements ServerPDUEventListener {

    public static final int CONN_DEF_RECV_TIMEOUT = 20 * 1000;
    public static final int CONN_DEF_COMMS_TIMEOUT = 5 * 1000;
    public static final int ENQ_LINK_RESP_TIMEOUT = 10 * 1000;
    private final SMSCNodeConfiguration cfg;
    private final String systemId;
    private final int enqLinkFreq;
    private final int reconnectInterval;
    private final String sourceAddress;
    private volatile Session session;
    private volatile Status status;
    private final String sock;
    private volatile int sequenceNumber;
    private volatile boolean waitingForResponse;
    private boolean waitedForResponse;
    private volatile boolean running;
    private final Object rLock = new Object();
    private final Object sLock = new Object();
    private final Logger logger = Logger.getLogger(SMSCConnection.class);

    public static enum Status {

        NOT_CONNECTED, CONNECTED;
    }

    public static enum MessageState {

        SCHEDULED((byte) 0), ENROUTE((byte) 1), DELIVERED((byte) 2), EXPIRED((byte) 3), DELETED((byte) 4),
        UNDELIVERABLE((byte) 5), ACCEPTED((byte) 6), UNKNOWN((byte) 7), REJECTED((byte) 8), SKIPPED((byte) 9);
        private final byte state;

        MessageState(byte state) {
            this.state = state;
        }

        public byte byteValue() {
            return state;
        }

        public static MessageState getState(byte state) {
            for (MessageState knownState : values()) {
                if (knownState.byteValue() == state) {
                    return knownState;
                }
            }
            return MessageState.UNKNOWN;
        }
    }

    public SMSCConnection(SMSCNodeConfiguration cfg) {
        this.cfg = cfg;
        this.systemId = cfg.getSystemId();
        this.enqLinkFreq = cfg.getEnqLinkFreq();
        this.reconnectInterval = cfg.getReconnectInterval();
        this.sourceAddress = cfg.getSourceAddress();
        this.status = Status.NOT_CONNECTED;
        this.session = null;
        this.sock = this.cfg.getServerAddress() + ":" + this.cfg.getServerPort();
        this.sequenceNumber = 0;
    }

    @Override
    public void run() {
        setRunning(true);
        logger.info(systemId + ": starting SMSC connection ...");
        while (isRunning()) {
            logger.info(systemId + ": waiting for " + reconnectInterval + " secs before binding");
            try {
                Thread.sleep(reconnectInterval * 1000);
            } catch (InterruptedException ie) {
                // interrupted
            }
            try {
                bind();
                System.err.println(systemId + ": Connection ok");
            } catch (Exception ex) {
                System.err.println(systemId + ": bind exception" + ex);
                unbind();
                continue;
            }
            while (isRunning()) {
                long startTime = System.currentTimeMillis();
                synchronized (this) {
                    waitingForResponse = true;
                    try {
                        session.enquireLink();
                        logger.info(systemId + ": EnquireLink sent");
                        this.wait(ENQ_LINK_RESP_TIMEOUT);
                        waitedForResponse = !waitingForResponse;
                    } catch (InterruptedException ie) {
                        // interrupted
                        waitedForResponse = false;
                    } catch (Exception ex) {
                        System.err.println(systemId + ": unable to send EnquireLink request "+ ex);
                        waitedForResponse = false;
                    }
                }
                long endTime = System.currentTimeMillis();
                if (waitedForResponse) {
                    System.err.println(systemId + ": EnquireLinkResponse received in " + (endTime - startTime) + " ms");
                } else {
                    System.err.println(systemId + ": no response for EnquireLink, reconnecting ...");
                    unbind();
                    break;
                }
                try {
                    Thread.sleep(enqLinkFreq * 1000);
                } catch (InterruptedException ie) {
                    // interrupted
                }
            }
        }
        System.err.println(systemId + ": SMSC connection stopped");
    }

    private void bind() throws Exception {
        System.err.println(systemId + ": connecting to SMSC on " + sock);
        BindRequest req = new BindTransciever();
        TCPIPConnection conn = new TCPIPConnection(cfg.getServerAddress(), cfg.getServerPort());
        conn.setReceiveTimeout(CONN_DEF_RECV_TIMEOUT);
        conn.setCommsTimeout(CONN_DEF_COMMS_TIMEOUT);
        session = new Session(conn);
        req.setSystemId(systemId);
        req.setPassword(cfg.getPassword());
        req.setSystemType(null);
        req.setInterfaceVersion(cfg.getIfaceVersion().byteValue());
        req.setAddressRange((String) null);
        logger.info(systemId + ": binding to SMSC on " + sock);
        BindResponse res = session.bind(req, this);
        int cmdStatus = res.getCommandStatus();
        if (cmdStatus != Data.ESME_ROK) {
            throw new RuntimeException(systemId + ": unable to connect/bind to SMSC on " + sock + ". Status = " + cmdStatus);
        }
        synchronized (this) {
            status = Status.CONNECTED;
        }
        System.err.println(systemId + ": connected to SMSC on " + sock);
    }

    private void unbind() {
        System.err.println(systemId + ": closing session with SMSC on " + sock);
        synchronized (this) {
            status = Status.NOT_CONNECTED;
            if (session != null) {
                try {
                    session.close();
                } catch (Exception ex) {
                   System.err.println(systemId + ": unable to close existing session "+ ex);
                }
                session = null;
            }
        }
        System.err.println(systemId + ": session closed with SMSC on " + sock);
    }

    public void handleEvent(ServerPDUEvent event) {
        PDU pdu = event.getPDU();
        logger.info(pdu);
        if (pdu instanceof EnquireLinkResp) {
            // EnquireLinkResp
            synchronized (this) {
                waitingForResponse = false;
                this.notifyAll();
            }
        } else if (pdu instanceof DeliverSM) {
            // DeliverSM
            handleDeliverSM((DeliverSM) pdu);
        } else if (pdu instanceof SubmitSMResp) {
            // SubmitSMResp
            handleSubmitSMResp((SubmitSMResp) pdu);
        } else {
            logger.warn(systemId + ": unhandled PDU: " + pdu.getClass().getName());
        }
    }

    private void handleDeliverSM(DeliverSM pdu) {
        int seq = pdu.getSequenceNumber();
        logger.debug(systemId + ": handling DeliverSM. seq = " + seq);
        Response res = null;
        try {
            if (pdu.hasReceiptedMessageId()) {
                // send DeliverSMResp in finally block
                res = pdu.getResponse();
                // handle state
                MessageState state = MessageState.getState(pdu.getMessageState());
                if (state.byteValue() < MessageState.DELIVERED.byteValue()) {
                    // internediate delivery report - nevers happens :)
                    logger.debug(systemId + ": intermediate delivery report ignored. seq = " + seq + ", state = " + state.toString());
                } else {
                    MessageStatus outboxMsgStatus;
                    if (state == MessageState.DELIVERED) {
                        outboxMsgStatus = MessageStatus.DELIVERED;
                    } else {
                        outboxMsgStatus = MessageStatus.FAILED;
                    }
                    String smscMsgId = pdu.getReceiptedMessageId();
                    logger.info(systemId + ": message delivered. seq = " + seq + ", state = " + state.toString() + ", smscMsgId = \"" + smscMsgId + ", statusId = \"" + outboxMsgStatus.toString() + "\"");
                }
                // set command status
                res.setCommandStatus(Data.ESME_ROK);
            } else {
                // do not send DeliverSMResp in finally block
                res = null;
                // aGWConnHandle
                String aGWConnHandle = systemId;
                // aOrigMSISDN
                String aOrigMSISDN = pdu.getSourceAddr().getAddress();
                // aMsgRef
                short aMsgRef = pdu.hasUserMessageReference() ? pdu.getUserMessageReference() : 0;
                // submit message to core
                logger.info(systemId + ": DeliverSM submitted. seq = " + seq + ", OrigMSISDN = " + aOrigMSISDN + ", msgRef = " + aMsgRef);
            }
        } catch (Exception ex) {
            logger.error(systemId + ": caught exception while handling DeliverSM. seq = " + seq, ex);
            // even if we didn't plan to send it, we have to...
            if (res == null) {
                res = pdu.getResponse();
            }
            // set response status
            res.setCommandStatus(Data.ESME_RUNKNOWNERR);
        } finally {
            if (res != null) {
                try {
                    send(res);
                } catch (Exception iex) {
                    logger.error(systemId + ": unable to send Response for DeliverSM to SMSC. seq = " + seq, iex);
                }
            }
        }
    }

    private void handleSubmitSMResp(SubmitSMResp pdu) {
        int seq = pdu.getSequenceNumber();
        logger.debug(systemId + ": handling SubmitSMResp. seq = " + seq);
        try {
            String smscMsgId = pdu.getMessageId();
            logger.info(systemId + ": message submited. seq = " + seq + ", smscMsgId = \"" + smscMsgId + "\", statusId = \"" + MessageStatus.SUBMITTED.toString() + "\"");
        } catch (Exception ex) {
            logger.error(systemId + ": caught exception while handling SubmitSMResp. seq = " + seq, ex);
        }
    }

    public void send(PDU pdu) throws Exception {
        if (pdu instanceof SubmitSM) {
            logger.info(pdu);
            int seq = pdu.getSequenceNumber();
            logger.debug(systemId + ": sending SubmitSM. seq = " + seq);
            synchronized (this) {
                if (status == Status.CONNECTED) {
                    session.submit((SubmitSM) pdu);
                } else {
                    throw new RuntimeException(systemId + ": SubmitSM failed: not connected. seq = " + seq);
                }
            }
            logger.debug(systemId + ": SubmitSM ok. seq = " + seq);
        } else if (pdu instanceof DeliverSMResp) {
            int seq = pdu.getSequenceNumber();
            logger.debug(systemId + ": sending DeliverSMResp. seq = " + seq);
            synchronized (this) {
                if (status == Status.CONNECTED) {
                    session.respond((DeliverSMResp) pdu);
                } else {
                    throw new RuntimeException(systemId + ": DeliverSMResp failed: not connected. seq = " + seq);
                }
            }
            logger.debug(systemId + ": DeliverSMResp ok. seq = " + seq);
        } else {
            logger.warn(pdu);
        }
    }

    public int submitSM(RawMessage msg, int seq, boolean requestDeliveryReport) {
        logger.debug(systemId + ": handling SubmitSM. seq = " + seq + ", messageType = " + msg.getMessageType().toString() + ", recipientMSISDN = \"" + msg.getRecipientMSISDN() + "\", deliveryReport = " + requestDeliveryReport + ", messageId = " + msg.getMessageId());
        SubmitSM pdu = new SubmitSM();
        pdu.setSequenceNumber(seq);
        try {
            pdu.setSourceAddr((byte) 5, (byte) 0, sourceAddress);
            pdu.setDestAddr((byte) 1, (byte) 1, msg.getRecipientMSISDN());
            pdu.setReplaceIfPresentFlag((byte) 0);
            pdu.setScheduleDeliveryTime(null);
            pdu.setValidityPeriod("000001000000000R");
            pdu.setPriorityFlag((byte) 0);
            if (msg.getMessageType() == MessageType.PLAIN) {
                pdu.setProtocolId((byte) 0x00);
                pdu.setDataCoding((byte) 0x00);
                pdu.setEsmClass((byte) 0x03);
                pdu.setShortMessage(msg.getRawMessage());
            } else {
                pdu.setProtocolId((byte) 0x7F);
                pdu.setDataCoding((byte) 0x16);
                pdu.setEsmClass((byte) 0x63);
                ShortMessage sm = new ShortMessage(200);
                sm.setData(new ByteBuffer(Base64.decode(msg.getRawMessage())));
                pdu.setShortMessageObj(sm);
            }
            if (requestDeliveryReport) {
                pdu.setRegisteredDelivery((byte) 1);
            } else {
                pdu.setRegisteredDelivery((byte) 0);
            }
            send(pdu);
            logger.info(systemId + ": SubmitSM submitted. seq = " + seq + ", messageId = " + msg.getMessageId());
            return 1;
        } catch (Exception ex) {
            logger.error(systemId + ": unable to submit SubmitSM to SMSC. seq = " + seq + ", messageId = " + msg.getMessageId(), ex);
            return 0;
        }
    }

    public synchronized Status getStatus() {
        return status;
    }

    public boolean isRunning() {
        synchronized (rLock) {
            return running;
        }
    }

    public void setRunning(boolean running) {
        synchronized (rLock) {
            this.running = running;
        }
    }

    public int getNextSequenceNumber() {
        synchronized (sLock) {
            if (sequenceNumber < Integer.MAX_VALUE) {
                return ++sequenceNumber;
            } else {
                return 1;
            }
        }
    }
}